// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2019-2025, The OpenROAD Authors

#include "odb/3dblox.h"

#include <cstddef>
#include <filesystem>
#include <map>
#include <set>
#include <sstream>
#include <string>
#include <unordered_set>
#include <vector>

#include "bmapParser.h"
#include "checker.h"
#include "dbvParser.h"
#include "dbvWriter.h"
#include "dbxParser.h"
#include "objects.h"
#include "odb/db.h"
#include "odb/dbTransform.h"
#include "odb/dbTypes.h"
#include "odb/defin.h"
#include "odb/defout.h"
#include "odb/geom.h"
#include "odb/lefin.h"
#include "odb/lefout.h"
#include "sta/Sta.hh"
#include "utl/Logger.h"
#include "utl/ScopedTemporaryFile.h"
namespace odb {

static std::map<std::string, std::string> dup_orient_map
    = {{"MY_R180", "MX"},
       {"MY_R270", "MX_R90"},
       {"MX_R180", "MY"},
       {"MX_R180", "MY_R90"},
       {"MZ_MY_R180", "MZ_MX"},
       {"MZ_MY_R270", "MZ_MX_R90"},
       {"MZ_MX_R180", "MZ_MY"},
       {"MZ_MX_R270", "MZ_MY_R90"}};

ThreeDBlox::ThreeDBlox(utl::Logger* logger, odb::dbDatabase* db, sta::Sta* sta)
    : logger_(logger), db_(db), sta_(sta)
{
}

void ThreeDBlox::readDbv(const std::string& dbv_file)
{
  DbvParser parser(logger_);
  DbvData data = parser.parseFile(dbv_file);
  if (db_->getDbuPerMicron() == 0) {
    db_->setDbuPerMicron(data.header.precision);
  } else {
    if (data.header.precision > db_->getDbuPerMicron()) {
      logger_->error(utl::ODB,
                     526,
                     "3DBV Parser Error: Precision is greater than dbu per "
                     "micron already set for database");
    } else if (db_->getDbuPerMicron() % data.header.precision != 0) {
      logger_->error(utl::ODB,
                     516,
                     "3DBV Parser Error: Database DBU per micron ({}) must be "
                     "a multiple of the precision ({}) in dbv file {}",
                     db_->getDbuPerMicron(),
                     data.header.precision,
                     dbv_file);
    }
  }
  readHeaderIncludes(data.header.includes);
  for (const auto& [_, chiplet] : data.chiplet_defs) {
    createChiplet(chiplet);
  }
}

void ThreeDBlox::readDbx(const std::string& dbx_file)
{
  DbxParser parser(logger_);
  DbxData data = parser.parseFile(dbx_file);
  readHeaderIncludes(data.header.includes);
  dbChip* chip = createDesignTopChiplet(data.design);
  for (const auto& [_, chip_inst] : data.chiplet_instances) {
    createChipInst(chip_inst);
  }
  for (const auto& [_, connection] : data.connections) {
    createConnection(connection);
  }
  calculateSize(db_->getChip());
  db_->triggerPostRead3Dbx(chip);
  check();
}

void ThreeDBlox::check()
{
  Checker checker(logger_);
  checker.check(db_->getChip());
}

namespace {
std::unordered_set<odb::dbTech*> getUsedTechs(odb::dbChip* chip)
{
  std::unordered_set<odb::dbTech*> techs;
  for (auto inst : chip->getChipInsts()) {
    if (inst->getMasterChip()->getTech() != nullptr) {
      techs.insert(inst->getMasterChip()->getTech());
    }
  }
  return techs;
}
std::unordered_set<odb::dbLib*> getUsedLibs(odb::dbChip* chip)
{
  std::unordered_set<odb::dbLib*> libs;
  for (auto inst : chip->getChipInsts()) {
    auto master_chip = inst->getMasterChip();
    if (master_chip->getBlock() != nullptr) {
      for (auto inst : master_chip->getBlock()->getInsts()) {
        libs.insert(inst->getMaster()->getLib());
      }
    }
  }
  return libs;
}
std::string getResultsDirectoryPath(const std::string& file_path)
{
  std::string current_dir_path;
  auto path = std::filesystem::path(file_path);
  if (path.has_parent_path()) {
    current_dir_path = path.parent_path().string() + "/";
  }
  return current_dir_path;
}
}  // namespace
void ThreeDBlox::writeDbv(const std::string& dbv_file, odb::dbChip* chip)
{
  if (chip == nullptr) {
    return;
  }
  ///////////Results Directory Path ///////////
  std::string current_dir_path = getResultsDirectoryPath(dbv_file);
  ////////////////////////////////////////////

  for (auto inst : chip->getChipInsts()) {
    auto master_chip = inst->getMasterChip();
    if (master_chip->getChipType() == odb::dbChip::ChipType::HIER) {
      writeDbx(current_dir_path + master_chip->getName() + ".3dbx",
               master_chip);
    }
  }
  // write used techs
  for (auto tech : getUsedTechs(chip)) {
    if (written_techs_.find(tech) != written_techs_.end()) {
      continue;
    }
    written_techs_.insert(tech);
    std::string tech_file_path = current_dir_path + tech->getName() + ".lef";
    utl::OutStreamHandler stream_handler(tech_file_path.c_str());
    odb::lefout lef_writer(logger_, stream_handler.getStream());
    lef_writer.writeTech(tech);
  }
  // write used libs
  for (auto lib : getUsedLibs(chip)) {
    if (written_libs_.find(lib) != written_libs_.end()) {
      continue;
    }
    written_libs_.insert(lib);
    std::string lib_file_path = current_dir_path + lib->getName() + "_lib.lef";
    utl::OutStreamHandler stream_handler(lib_file_path.c_str());
    odb::lefout lef_writer(logger_, stream_handler.getStream());
    lef_writer.writeLib(lib);
  }

  DbvWriter writer(logger_, db_);
  writer.writeChiplet(dbv_file, chip);
}

void ThreeDBlox::writeDbx(const std::string& dbx_file, odb::dbChip* chip)
{
  if (chip == nullptr) {
    return;
  }
  // TODO: implement
  std::string current_dir_path = getResultsDirectoryPath(dbx_file);
  writeDbv(current_dir_path + chip->getName() + ".3dbv", chip);
}

void ThreeDBlox::calculateSize(dbChip* chip)
{
  Rect box;
  box.mergeInit();
  for (auto inst : chip->getChipInsts()) {
    box.merge(inst->getBBox());
  }
  chip->setWidth(box.dx());
  chip->setHeight(box.dy());
}

void ThreeDBlox::readHeaderIncludes(const std::vector<std::string>& includes)
{
  for (const auto& include : includes) {
    if (include.find(".3dbv") != std::string::npos) {
      readDbv(include);
    } else if (include.find(".3dbx") != std::string::npos) {
      readDbx(include);
    }
  }
}

dbChip::ChipType getChipType(const std::string& type, utl::Logger* logger)
{
  if (type == "die") {
    return dbChip::ChipType::DIE;
  }
  if (type == "rdl") {
    return dbChip::ChipType::RDL;
  }
  if (type == "ip") {
    return dbChip::ChipType::IP;
  }
  if (type == "substrate") {
    return dbChip::ChipType::SUBSTRATE;
  }
  if (type == "hier") {
    return dbChip::ChipType::HIER;
  }
  logger->error(
      utl::ODB, 527, "3DBV Parser Error: Invalid chip type: {}", type);
}

std::string getFileName(const std::string& tech_file_path)
{
  std::filesystem::path tech_file_path_fs(tech_file_path);
  return tech_file_path_fs.stem().string();
}

void ThreeDBlox::createChiplet(const ChipletDef& chiplet)
{
  dbTech* tech = nullptr;

  // Read tech LEF file
  if (!chiplet.external.tech_lef_files.empty()) {
    if (chiplet.external.tech_lef_files.size() > 1) {
      logger_->error(
          utl::ODB,
          529,
          "3DBV Parser Error: Multiple tech LEF files are not supported");
    }
    auto tech_file = chiplet.external.tech_lef_files[0];
    auto tech_name = getFileName(tech_file);
    odb::lefin lef_reader(db_, logger_, false);
    tech = db_->findTech(tech_name.c_str());
    if (tech == nullptr) {
      lef_reader.createTechAndLib(
          tech_name.c_str(), tech_name.c_str(), tech_file.c_str());
      tech = db_->findTech(tech_name.c_str());
    }
  }
  // Read LEF files

  for (const auto& lef_file : chiplet.external.lef_files) {
    auto lib_name = getFileName(lef_file);
    odb::lefin lef_reader(db_, logger_, false);
    lef_reader.createLib(tech, lib_name.c_str(), lef_file.c_str());
  }
  if (sta_ != nullptr) {
    for (const auto& liberty_file : chiplet.external.lib_files) {
      sta_->readLiberty(
          liberty_file.c_str(), sta_->cmdCorner(), sta::MinMaxAll::all(), true);
    }
  }
  // Check if chiplet already exists
  auto chip = db_->findChip(chiplet.name.c_str());
  if (chip != nullptr) {
    if (chip->getChipType() != getChipType(chiplet.type, logger_)
        || chip->getChipType() != dbChip::ChipType::HIER) {
      logger_->error(utl::ODB,
                     530,
                     "3DBV Parser Error: Chiplet {} already exists",
                     chiplet.name);
    }
    // chiplet already exists, update it
  } else {
    chip = dbChip::create(
        db_, tech, chiplet.name, getChipType(chiplet.type, logger_));
  }
  // Read DEF file
  if (!chiplet.external.def_file.empty()) {
    odb::defin def_reader(db_, logger_, odb::defin::DEFAULT);
    std::vector<odb::dbLib*> search_libs;
    for (odb::dbLib* lib : db_->getLibs()) {
      search_libs.push_back(lib);
    }
    // No callbacks here as we are going to give one postRead3Dbx later
    def_reader.readChip(search_libs,
                        chiplet.external.def_file.c_str(),
                        chip,
                        /*issue_callback*/ false);
  }
  chip->setWidth(chiplet.design_width * db_->getDbuPerMicron());
  chip->setHeight(chiplet.design_height * db_->getDbuPerMicron());
  chip->setThickness(chiplet.thickness * db_->getDbuPerMicron());
  chip->setShrink(chiplet.shrink);
  chip->setTsv(chiplet.tsv);

  chip->setScribeLineEast(chiplet.scribe_line_right * db_->getDbuPerMicron());
  chip->setScribeLineWest(chiplet.scribe_line_left * db_->getDbuPerMicron());
  chip->setScribeLineNorth(chiplet.scribe_line_top * db_->getDbuPerMicron());
  chip->setScribeLineSouth(chiplet.scribe_line_bottom * db_->getDbuPerMicron());

  chip->setSealRingEast(chiplet.seal_ring_right * db_->getDbuPerMicron());
  chip->setSealRingWest(chiplet.seal_ring_left * db_->getDbuPerMicron());
  chip->setSealRingNorth(chiplet.seal_ring_top * db_->getDbuPerMicron());
  chip->setSealRingSouth(chiplet.seal_ring_bottom * db_->getDbuPerMicron());

  chip->setOffset(Point(chiplet.offset.x * db_->getDbuPerMicron(),
                        chiplet.offset.y * db_->getDbuPerMicron()));
  if (chip->getChipType() != dbChip::ChipType::HIER
      && chip->getBlock() == nullptr) {
    // blackbox stage, create block
    auto block = odb::dbBlock::create(chip, chiplet.name.c_str());
    block->setDieArea(Rect(0, 0, chip->getWidth(), chip->getHeight()));
    block->setCoreArea(Rect(0, 0, chip->getWidth(), chip->getHeight()));
  }
  for (const auto& [_, region] : chiplet.regions) {
    createRegion(region, chip);
  }
}

dbChipRegion::Side getChipRegionSide(const std::string& side,
                                     utl::Logger* logger)
{
  if (side == "front") {
    return dbChipRegion::Side::FRONT;
  }
  if (side == "back") {
    return dbChipRegion::Side::BACK;
  }
  if (side == "internal") {
    return dbChipRegion::Side::INTERNAL;
  }
  if (side == "internal_ext") {
    return dbChipRegion::Side::INTERNAL_EXT;
  }
  logger->error(
      utl::ODB, 528, "3DBV Parser Error: Invalid chip region side: {}", side);
}
void ThreeDBlox::createRegion(const ChipletRegion& region, dbChip* chip)
{
  dbTechLayer* layer = nullptr;
  if (!region.layer.empty()) {
    // TODO: add layer
  }
  dbChipRegion* chip_region = dbChipRegion::create(
      chip, region.name, getChipRegionSide(region.side, logger_), layer);
  Rect box;
  box.mergeInit();
  for (const auto& coord : region.coords) {
    box.merge(Point(coord.x * db_->getDbuPerMicron(),
                    coord.y * db_->getDbuPerMicron()),
              box);
  }
  chip_region->setBox(box);
  // Read bump map file
  if (!region.bmap.empty()) {
    BmapParser parser(logger_);
    BumpMapData data = parser.parseFile(region.bmap);
    for (const auto& entry : data.entries) {
      createBump(entry, chip_region);
    }
  }
}

void ThreeDBlox::createBump(const BumpMapEntry& entry,
                            dbChipRegion* chip_region)
{
  auto chip = chip_region->getChip();
  auto block = chip->getBlock();
  auto inst = block->findInst(entry.bump_inst_name.c_str());
  if (inst == nullptr) {
    // create inst
    auto master = db_->findMaster(entry.bump_cell_type.c_str());
    if (master == nullptr) {
      logger_->error(utl::ODB,
                     531,
                     "3DBV Parser Error: Bump cell type {} not found",
                     entry.bump_cell_type);
    }
    if (master->getLib()->getTech() != chip->getTech()) {
      logger_->error(utl::ODB,
                     532,
                     "3DBV Parser Error: Bump cell type {} is not in the same "
                     "tech as the chip region {}/{}",
                     entry.bump_cell_type,
                     chip->getName(),
                     chip_region->getName());
    }
    inst = dbInst::create(block, master, entry.bump_inst_name.c_str());
  }
  auto bump = dbChipBump::create(chip_region, inst);
  Rect bbox;
  inst->getMaster()->getPlacementBoundary(bbox);
  inst->setOrigin((entry.x * db_->getDbuPerMicron()) - bbox.xCenter(),
                  (entry.y * db_->getDbuPerMicron()) - bbox.yCenter());
  inst->setPlacementStatus(dbPlacementStatus::FIRM);
  if (entry.net_name != "-") {
    auto net = block->findNet(entry.net_name.c_str());
    if (net == nullptr) {
      logger_->error(utl::ODB,
                     534,
                     "3DBV Parser Error: Bump net {} not found",
                     entry.net_name);
    }
    bump->setNet(net);
    inst->getITerms().begin()->connect(net);
  }
  if (entry.port_name != "-") {
    auto bterm = block->findBTerm(entry.port_name.c_str());
    if (bterm == nullptr) {
      logger_->error(utl::ODB,
                     533,
                     "3DBV Parser Error: Bump port {} not found",
                     entry.port_name);
    }
    bump->setBTerm(bterm);
    if (bump->getNet()) {
      bterm->connect(bump->getNet());
    }
  }
}

dbChip* ThreeDBlox::createDesignTopChiplet(const DesignDef& design)
{
  dbChip* chip
      = dbChip::create(db_, nullptr, design.name, dbChip::ChipType::HIER);
  db_->setTopChip(chip);
  return chip;
}

void ThreeDBlox::createChipInst(const ChipletInst& chip_inst)
{
  auto chip = db_->findChip(chip_inst.reference.c_str());
  if (chip == nullptr) {
    logger_->error(utl::ODB,
                   519,
                   "3DBX Parser Error: Chiplet instance reference {} not found "
                   "for chip inst {}",
                   chip_inst.reference,
                   chip_inst.name);
  }
  dbChipInst* inst = dbChipInst::create(db_->getChip(), chip, chip_inst.name);
  inst->setLoc(Point3D(chip_inst.loc.x * db_->getDbuPerMicron(),
                       chip_inst.loc.y * db_->getDbuPerMicron(),
                       chip_inst.z * db_->getDbuPerMicron()));
  auto orient_str = chip_inst.orient;
  if (dup_orient_map.find(orient_str) != dup_orient_map.end()) {
    orient_str = dup_orient_map[orient_str];
  }
  auto orient = dbOrientType3D::fromString(orient_str);
  if (!orient.has_value()) {
    logger_->error(utl::ODB,
                   525,
                   "3DBX Parser Error: Invalid orient {} for chip inst {}",
                   chip_inst.orient,
                   chip_inst.name);
  }
  inst->setOrient(orient.value());
}
std::vector<std::string> splitPath(const std::string& path)
{
  std::vector<std::string> parts;
  std::istringstream stream(path);
  std::string part;

  while (std::getline(stream, part, '/')) {
    if (!part.empty()) {
      parts.push_back(part);
    }
  }

  return parts;
}

dbChipRegionInst* ThreeDBlox::resolvePath(const std::string& path,
                                          std::vector<dbChipInst*>& path_insts)
{
  if (path == "~") {
    return nullptr;
  }
  // Split the path by '/'
  std::vector<std::string> path_parts = splitPath(path);

  if (path_parts.empty()) {
    logger_->error(utl::ODB, 524, "3DBX Parser Error: Invalid path {}", path);
  }

  // The last part should contain ".regions.regionName"
  std::string last_part = path_parts.back();
  size_t regions_pos = last_part.find(".regions.");
  if (regions_pos == std::string::npos) {
    return nullptr;  // Invalid format
  }

  // Extract chip instance name and region name from last part
  std::string last_chip_inst = last_part.substr(0, regions_pos);
  std::string region_name = last_part.substr(regions_pos + 9);

  // Replace the last part with just the chip instance name
  path_parts.back() = last_chip_inst;

  // TODO: Traverse hierarchy and find region
  path_insts.reserve(path_parts.size());
  dbChip* curr_chip = db_->getChip();
  dbChipInst* curr_chip_inst = nullptr;
  for (const auto& inst_name : path_parts) {
    curr_chip_inst = curr_chip->findChipInst(inst_name);
    if (curr_chip_inst == nullptr) {
      logger_->error(utl::ODB,
                     522,
                     "3DBX Parser Error: Chip instance {} not found in path {}",
                     inst_name,
                     path);
    }
    path_insts.push_back(curr_chip_inst);
    curr_chip = curr_chip_inst->getMasterChip();
  }
  auto region = curr_chip_inst->findChipRegionInst(region_name);
  if (region == nullptr) {
    logger_->error(utl::ODB,
                   523,
                   "3DBX Parser Error: Chip region {} not found in path {}",
                   region_name,
                   path);
  }
  return region;
}
void ThreeDBlox::createConnection(const Connection& connection)
{
  auto top_path = connection.top;
  auto bottom_path = connection.bot;
  std::vector<dbChipInst*> top_region_path;
  std::vector<dbChipInst*> bottom_region_path;
  auto top_region = resolvePath(top_path, top_region_path);
  auto bottom_region = resolvePath(bottom_path, bottom_region_path);
  auto conn = odb::dbChipConn::create(connection.name,
                                      db_->getChip(),
                                      top_region_path,
                                      top_region,
                                      bottom_region_path,
                                      bottom_region);
  conn->setThickness(connection.thickness * db_->getDbuPerMicron());
}

void ThreeDBlox::readBMap(const std::string& bmap_file)
{
  dbBlock* block = db_->getChip()->getBlock();

  BmapParser parser(logger_);
  BumpMapData data = parser.parseFile(bmap_file);
  std::vector<odb::dbInst*> bumps;
  bumps.reserve(data.entries.size());
  for (const auto& entry : data.entries) {
    bumps.push_back(createBump(entry, block));
  }

  struct BPinInfo
  {
    dbTechLayer* layer = nullptr;
    odb::Rect rect;
  };

  // Populate where the bpins should be made
  std::map<odb::dbMaster*, BPinInfo> bpininfo;
  for (dbInst* inst : bumps) {
    dbMaster* master = inst->getMaster();
    if (bpininfo.find(master) != bpininfo.end()) {
      continue;
    }

    odb::dbTechLayer* max_layer = nullptr;
    std::set<odb::Rect> top_shapes;

    for (dbMTerm* mterm : master->getMTerms()) {
      for (dbMPin* mpin : mterm->getMPins()) {
        for (dbBox* geom : mpin->getGeometry()) {
          auto* layer = geom->getTechLayer();
          if (layer == nullptr) {
            continue;
          }
          if (max_layer == nullptr) {
            max_layer = layer;
            top_shapes.insert(geom->getBox());
          } else if (max_layer->getRoutingLevel() <= layer->getRoutingLevel()) {
            if (max_layer->getRoutingLevel() < layer->getRoutingLevel()) {
              top_shapes.clear();
            }
            max_layer = layer;
            top_shapes.insert(geom->getBox());
          }
        }
      }
    }

    if (max_layer != nullptr) {
      odb::Rect master_box;
      master->getPlacementBoundary(master_box);
      const odb::Point center = master_box.center();
      const odb::Rect* top_shape_ptr = nullptr;
      for (const odb::Rect& shape : top_shapes) {
        if (shape.intersects(center)) {
          top_shape_ptr = &shape;
        }
      }

      if (top_shape_ptr == nullptr) {
        top_shape_ptr = &(*top_shapes.begin());
      }

      bpininfo.emplace(master, BPinInfo{max_layer, *top_shape_ptr});
    }
  }

  // create bpins
  for (dbInst* inst : bumps) {
    auto masterbpin = bpininfo.find(inst->getMaster());
    if (masterbpin == bpininfo.end()) {
      continue;
    }

    const BPinInfo& pin_info = masterbpin->second;

    const dbTransform xform = inst->getTransform();
    for (dbITerm* iterm : inst->getITerms()) {
      dbNet* net = iterm->getNet();
      if (net == nullptr) {
        continue;
      }
      dbBTerm* bterm = net->get1stBTerm();
      dbBPin* pin = dbBPin::create(bterm);
      Rect shape = pin_info.rect;
      xform.apply(shape);
      dbBox::create(pin,
                    pin_info.layer,
                    shape.xMin(),
                    shape.yMin(),
                    shape.xMax(),
                    shape.yMax());
      pin->setPlacementStatus(odb::dbPlacementStatus::FIRM);
      break;
    }
  }
}

dbInst* ThreeDBlox::createBump(const BumpMapEntry& entry, dbBlock* block)
{
  const int dbus = db_->getDbuPerMicron();
  dbInst* inst = block->findInst(entry.bump_inst_name.c_str());
  if (inst == nullptr) {
    // create inst
    dbMaster* master = db_->findMaster(entry.bump_cell_type.c_str());
    if (master == nullptr) {
      logger_->error(utl::ODB,
                     538,
                     "3DBV Parser Error: Bump cell type {} not found",
                     entry.bump_cell_type);
    }
    inst = dbInst::create(block, master, entry.bump_inst_name.c_str());
  }
  inst->setOrigin(entry.x * dbus, entry.y * dbus);
  inst->setPlacementStatus(dbPlacementStatus::FIRM);

  // Net entry doesn't make sense
  if (entry.net_name != "-") {
    dbNet* net = block->findNet(entry.net_name.c_str());
    if (net == nullptr) {
      logger_->error(utl::ODB,
                     539,
                     "3DBV Parser Error: Bump net {} not found",
                     entry.net_name);
    }
    for (odb::dbITerm* iterm : inst->getITerms()) {
      iterm->connect(net);
    }
  }
  // Port already on the net, so skip

  return inst;
}

}  // namespace odb
